package org.xmlbinder;

import java.util.*;
import java.sql.Timestamp;
import java.lang.reflect.*;
import java.math.BigDecimal;

/** This is the class which
 *  reads the annotation of an input class tree and caches the information<br>
 * This class reads the data of  a bean while the FieldAnnotationProcessor does it for a field
 * @author Noble Paul (noble.paul@gmail.com)
 *         Date: Sep 4, 2007
 *         Time: 10:10:26 AM
 */
class BeanAnnotationProcessor extends BeanInjector {

    BeanAnnotationProcessor(FieldAnnotationProcessor bf) {
        this.clz = bf.type;
        parentField = bf;
    }

    BeanAnnotationProcessor() {
    }

    /**Constructor that is the starting point for meta data processing
     * @param clz
     */
    BeanAnnotationProcessor(Class clz) {
        this.clz = clz;
        if (this.clz.isAnnotationPresent(Element.class)) {
            Element e = (Element) this.clz.getAnnotation(Element.class);
            name = e.value();
        } /*if (JAXB_IN_CLASSPATH && this.clz.isAnnotationPresent(XmlRootElement.class)) {
            XmlRootElement e = (XmlRootElement) this.clz.getAnnotation(XmlRootElement.class);
            name = e.name();
        }*/
        if (name == null || "".equals(name) || "##default".equals(name)) {
            name = this.clz.getName();
            name = name.substring(name.lastIndexOf('.') + 1);
        }
        buildMeta(null);
        postInit(Collections.<FieldAnnotationProcessor>emptySet());
    }

    /**Read the class tree and builds the datastructure for a bean . This is called for skiplevel mapping only
     * @param names The names between the '/'
     * @param bf the root beanfield where it all started
     * @param immedieteParent
     */
    void buildMeta(LinkedList<String> names, FieldAnnotationProcessor bf, FieldAnnotationProcessor immedieteParent) {
        parentField = immedieteParent;
        FieldAnnotationProcessor beanField = new FieldAnnotationProcessor(names, bf, this);
        if(beanField.isEndTag){
            addEndTag(beanField);
        } else if(beanField.isStartTag){
            addStartTag(beanField);
        }else if (beanField.isElem) {
            elemFields.put(beanField.name, beanField);
        } else {
            attrFields.put(beanField.name, beanField);
        }
    }

    private void addStartTag(FieldAnnotationProcessor beanField) {
        if(startTagMethods == null) startTagMethods =  new HashMap<String, FieldAnnotationProcessor>();
        startTagMethods.put(beanField.name,beanField);
    }

    private void addEndTag(FieldAnnotationProcessor beanField) {
        if(endTagMethods == null) endTagMethods = new HashMap<String, FieldAnnotationProcessor>();
        endTagMethods.put(beanField.name,beanField);
    }

    /**This method is invoked after the data tree is built. if takes care of the cases where multilevel mapping is present with wildcards
     * @param wildCards
     */
    void postInit(Set<FieldAnnotationProcessor> wildCards) {
        Set<FieldAnnotationProcessor> wildCardsWithSubWildCards = new LinkedHashSet<FieldAnnotationProcessor>();
        for (FieldAnnotationProcessor subWildCard : wildCards) {
            if (elemWildcard == null) {
                elemWildcard = subWildCard;
                elemFields.put(elemWildcard.name, elemWildcard);
            } else {
                for (FieldAnnotationProcessor beanField : subWildCard.bean.elemFields.values()) {
                    if ("*".equals(beanField.name)) {
                        wildCardsWithSubWildCards.add(beanField);
                    } else if (elemWildcard.bean.elemFields.get(beanField.name) == null) {
                        elemWildcard.bean.elemFields.put(beanField.name, beanField);
                    }
                }
            }
        }

        for (FieldAnnotationProcessor bf : elemFields.values()) {
            if (elemWildcard != null && bf != elemWildcard) {
                for (FieldAnnotationProcessor wildcardFld : elemWildcard.bean.elemFields.values()) {
                    if ("*".equals(wildcardFld.name)) {
                        wildCardsWithSubWildCards.add(wildcardFld);
                    } else if (bf.bean.elemFields.get(wildcardFld.name) == null) {
                        bf.bean.elemFields.put(wildcardFld.name, wildcardFld);
                    }
                }
            }
            bf.bean.postInit(wildCardsWithSubWildCards);
        }
    }

    static boolean isSimple(Class clz) {
        return (clz == String.class) || clz.isPrimitive() || (clz == Integer.class) || (clz == Double.class) ||
                (clz == Float.class) || (clz == Long.class) || (clz == Short.class) || (clz == Byte.class) ||
                (clz == Boolean.class) || (clz == Timestamp.class) || (clz == BigDecimal.class);
    }

    /**Traverse the class tree , capture the metadata and build tre tree data structure
     * @param beanField
     * @return
     */
    public BeanAnnotationProcessor buildMeta(FieldAnnotationProcessor beanField) {
        if (isSimple(clz)) {
            isSimpleType = true; //Primitives and their object counterparts and String must be simple type
            cdata = beanField;
            return this;
        }
        try {
            clz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Invalid class . Unable to create an instance of class (must have a no-arg constructor): " + clz);
        }
        ArrayList<AccessibleObject> members = new ArrayList<AccessibleObject>();
        members.addAll(Arrays.asList(clz.getDeclaredFields()));
        members.addAll(Arrays.asList(clz.getDeclaredMethods()));
        FieldAnnotationProcessor bf = null;
        String[] multiNames;
        for (AccessibleObject member : members) {
            if (member.isAnnotationPresent(Attribute.class) ) {
                String attrName = member.getAnnotation(Attribute.class).value();

                multiNames = checkMultiNames(attrName);
                for (String aName : multiNames) {
                    bf = new FieldAnnotationProcessor(aName, this, member);
                    if (bf.isElem) {
                        checkForDuplicateAndAdd(bf);
                        continue;
                    }
                    attrFields.put(bf.name, bf);
                }
            }
            if (member.isAnnotationPresent(Element.class)) {
                String elemName = member.getAnnotation(Element.class).value();

                multiNames = checkMultiNames(elemName);
                for (String aName : multiNames) {
                    bf = new FieldAnnotationProcessor(aName, this, member);
                    checkForDuplicateAndAdd(bf);
                }
            }
            if (member.isAnnotationPresent(Cdata.class) ) {
                bf = new FieldAnnotationProcessor(null, this, member);
                cdata = bf;
            }
            checkForSetParent(member, beanField);
        }
        return this;
    }

    private void checkForSetParent(AccessibleObject member, FieldAnnotationProcessor bf) {
        if (! (member instanceof Method)) return;
        Method method = (Method) member;
        if(method.getName().equals("setParent") && method.getParameterTypes().length ==1){
            Class argType = method.getParameterTypes()[0];
            if(bf!=null && bf.parent!=null && argType == bf.parent.clz){
                this.setParentMethod = method;
            }
        }
    }

    /**cjecks if the same field is mapped to multiple tags/attributes (for comma separated items)
     * @param s
     * @return
     */
    private String[] checkMultiNames(String s) {
        if ("##default".equals(s)) return new String[1];
        String strs[] = s.split(",");
        if (strs.length < 2) return new String[1];
        return strs;
    }

    private void checkForDuplicateAndAdd(FieldAnnotationProcessor bf) {
        if(bf.isEndTag){
            addEndTag(bf);
        } else if(bf.isStartTag){
            addStartTag(bf);
        } else{
            if (elemFields.get(bf.name) != null) return;
            elemFields.put(bf.name, bf);
        }
    }

}
